import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import java.util.*;

/**
 * 类名：A登陆界面
 * 描述：创建一个聊天室客户端,连接服务器并实现聊天功能
 */
public class A登陆界面 extends JFrame implements ActionListener {  //定义了一个名为 "A登陆界面" 的类，
    // 继承了 JFrame 类，并实现了 ActionListener 接口。
    JFrame clientFrame = new JFrame(C常量.APP_SERVER_NAME);
//创建了一个名为 clientFrame 的 JFrame 对象，并设置其标题为 Constants.APP_SERVER_NAME。
    GridBagLayout gl;
    BorderLayout bdl;
    GridBagConstraints gbc;
//定义了三个布局和布局约束相关的变量。
    // 聊天界面
    JPanel pnlBack, pnlTalk;
    JButton btnTalk;
    JTextArea txtViewTalk;
    JLabel lblTalk, lblTo;
    JComboBox listOnline;
//定义了用于聊天界面的一些 UI 组件，如面板 (JPanel)、按钮 (JButton)、
// 文本区域 (JTextArea)、标签 (JLabel) 和下拉列表 (JComboBox)。
    // 登陆界面
    JPanel pnlLogin;

    JLabel lblServerIP;
    JLabel lblName;
    JLabel lblPassword;

    JTextField txtTalk;
    JTextField txtServerIP;
    JTextField txtName;

    JPasswordField txtPassword;

    JButton btnLogin;
    JButton btnReg;
    JButton btnExit;

    JDialog dialogLogin = new JDialog(this, C常量.LOGIN, true);
//定义了用于登录界面的一些 UI 组件，包括面板 (JPanel)、标签 (JLabel)、文本框 (JTextField)、密码框 (JPasswordField)
// 和按钮 (JButton)。同时创建了一个名为 dialogLogin 的模态对话框 (JDialog)。
    Socket socket = null;
    BufferedReader in = null;
    PrintWriter out = null;
//定义了与服务器通信所需的套接字 (Socket)、输入流 (BufferedReader) 和输出流 (PrintWriter)。
    String strSend;
    String strReceive;
    String strKey;
    String strStatus;
    private StringTokenizer st;
//定义了一些字符串变量和一个 StringTokenizer 对象，用于解析字符串。
    public A登陆界面() {
        // 初始化
        gl = new GridBagLayout();
        bdl = new BorderLayout();
        gbc = new GridBagConstraints();
        pnlBack = (JPanel) getContentPane();
        pnlBack.setLayout(bdl);
/*构造函数 A登陆界面，用于创建登录界面的GUI。
创建并初始化布局管理器 GridBagLayout、BorderLayout 和 GridBagConstraints。
获取内容面板并设置其布局为 BorderLayout。*/
        // 初始化控件
        pnlLogin = new JPanel();
        pnlLogin.setLayout(gl);
/*创建面板 pnlLogin，并将其布局设置为 GridBagLayout。*/
        lblServerIP = new JLabel(C常量.SERVER_IP);
        lblName = new JLabel(C常量.USER_NAME);
        lblPassword = new JLabel(C常量.PASSWORD);
        txtServerIP = new JTextField(12);
        txtName = new JTextField(12);
        txtPassword = new JPasswordField(12);
/*创建标签 lblServerIP、lblName 和 lblPassword，并为其设置显示文本。
创建文本框 txtServerIP、txtName 和 txtPassword，并为其设置指定的列数。*/
        txtServerIP.setText("127.0.0.1");
//设置文本框 txtServerIP 的默认文本为 "127.0.0.1"。
        btnLogin = new JButton(C常量.LOGIN);
        btnReg = new JButton(C常量.REGISTER);
        btnExit = new JButton(C常量.EXIT);

        btnTalk = new JButton(C常量.SEND);
        lblTalk = new JLabel(C常量.SPEAK);

        lblTo = new JLabel(" To :");
        txtTalk = new JTextField(30);
        pnlTalk = new JPanel();
        txtViewTalk = new JTextArea(18, 40);
        listOnline = new JComboBox();
        txtViewTalk.setForeground(Color.blue);
        btnTalk.addActionListener(this);

        btnLogin.addActionListener(this);
        btnReg.addActionListener(this);
        btnExit.addActionListener(this);
/*创建按钮 btnLogin、btnReg 和 btnExit，并为其设置显示文本。
创建按钮 btnTalk、标签 lblTalk、标签 lblTo、文本框 txtTalk、面板 pnlTalk、文本域 txtViewTalk 和下拉列表 listOnline。
为按钮 btnTalk 添加事件监听器。
为按钮 btnLogin、btnReg 和 btnExit 添加事件监听器。*/
        listOnline.addItem("All");
/*向下拉列表 listOnline 添加项 "All"。*/
        pnlTalk.add(lblTalk);
        pnlTalk.add(txtTalk);
        pnlTalk.add(lblTo);
        pnlTalk.add(listOnline);
        pnlTalk.add(btnTalk);
        pnlBack.add("Center", txtViewTalk);
        pnlBack.add("South", pnlTalk);
        pnlTalk.setBackground(Color.cyan);
        btnTalk.setEnabled(false);
/*将标签 lblTalk、文本框 txtTalk、标签 lblTo、下拉列表 listOnline 和按钮 btnTalk 添加到面板 pnlTalk 中。
将文本域 txtViewTalk 添加到内容面板 pnlBack 的中央位置。
将面板 pnlTalk 添加到内容面板 pnlBack 的南部位置。
设置面板 pnlTalk 的背景颜色为青色。
禁用按钮 btnTalk。*/
        clientFrame.getContentPane().add(pnlBack);
        clientFrame.setSize(600, 450);
        clientFrame.setVisible(true);
        clientFrame.setResizable(false);
        clientFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
/*将内容面板 pnlBack 添加到客户端窗体 clientFrame 的内容面板。
设置客户端窗体 clientFrame 的大小为 600x450 像素。
设置客户端窗体 clientFrame 可见。
禁止客户端窗体 clientFrame 调整大小。
设置客户端窗体 clientFrame 的默认关闭操作为退出应用程序。*/
        // 登陆对话框初始化
        dialogLogin.getContentPane().setLayout(new FlowLayout());
        dialogLogin.getContentPane().add(lblServerIP);
        dialogLogin.getContentPane().add(txtServerIP);
        dialogLogin.getContentPane().add(lblName);
        dialogLogin.getContentPane().add(txtName);
        dialogLogin.getContentPane().add(lblPassword);
        dialogLogin.getContentPane().add(txtPassword);
        dialogLogin.getContentPane().add(btnLogin);
        dialogLogin.getContentPane().add(btnReg);
        dialogLogin.getContentPane().add(btnExit);

        dialogLogin.setBounds(300, 300, 250, 200);
        dialogLogin.getContentPane().setBackground(Color.gray);
        dialogLogin.show(true);
    }
    /*设置对话框 dialogLogin 的内容面板布局为 FlowLayout。
将标签 lblServerIP、文本框 txtServerIP、标签 lblName、文本框 txtName、标签 lblPassword、文本框 txtPassword、按钮 btnLogin、按钮 btnReg 和按钮 btnExit 添加到对话框 dialogLogin 的内容面板。
设置对话框 dialogLogin 的位置和大小。
设置对话框 dialogLogin 的内容面板背景颜色为灰色。
显示对话框 dialogLogin。
*/
//定义了 A登陆界面 类的构造函数。在构造函数中，初始化了布局和布局约束相关的变量，创建了登录界面的面板 (pnlLogin)，并设置了布局。
    public static void main(String[] args) {
        new A登陆界面();
    }
//main 方法，程序的入口点。创建一个 A登陆界面 对象，以启动应用程序。
    /**
     * 建立与服务端通信的套接字
     */
    void connectServer() {
        try {
            socket = new Socket(txtServerIP.getText(), 8888);
            in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
                    socket.getOutputStream())), true);
        } catch (ConnectException e) {
            JOptionPane.showMessageDialog(this, "连接服务器失败!", "ERROE",
                    JOptionPane.INFORMATION_MESSAGE);
            txtServerIP.setText("");
            System.out.println(e);
        } catch (Exception e) {
            System.out.println(e);
        }
    }
//connectServer 方法，用于建立与服务器的连接。
    /**
     * 弹出窗口
     *
     * @param strWarning
     * @param strTitle
     */
    public void popWindows(String strWarning, String strTitle) {
        JOptionPane.showMessageDialog(this, strWarning, strTitle,
                JOptionPane.INFORMATION_MESSAGE);
    }
//popWindows 方法，用于弹出窗口显示警告或消息。
    private void initLogin() throws IOException {
        strReceive = in.readLine();
        st = new StringTokenizer(strReceive, "|");
        strKey = st.nextToken();
        if (strKey.equals("login")) {
            strStatus = st.nextToken();
            if (strStatus.equals("succeed")) {
                btnLogin.setEnabled(false);
                btnTalk.setEnabled(true);
                pnlLogin.setVisible(false);
                dialogLogin.dispose();
                new ClientThread(socket);
                out.println("init|online");
            }//initLogin 方法，在登录成功后初始化客户端界面。
            popWindows(strKey + " " + strStatus + "!", "Login");
        }
        if (strKey.equals("warning")) {
            strStatus = st.nextToken();
            popWindows(strStatus, "Register");
        }
    }

    public void actionPerformed(ActionEvent evt) {
        Object obj = evt.getSource();
//actionPerformed 方法，用于处理按钮点击事件。ActionEvent 对象提供了有关事件源和操作的信息。
// 在方法开始时，通过 getSource 方法获取事件源对象，并将其存储在 obj 变量中。
        try {
            if (obj.equals(btnLogin)) {
                if ((txtServerIP.getText().length() > 0)
                        && (txtName.getText().length() > 0)
                        && (txtPassword.getText().length() > 0)) {
                    connectServer();
                    strSend = "login|" + txtName.getText() + "|"
                            + String.valueOf(txtPassword.getPassword());
                    out.println(strSend);
                    initLogin();
                } else {
                    popWindows("请输入完整信息", "ERROR");
                }
            } else if (obj.equals(btnReg)) {
                if ((txtName.getText().length() > 0)
                        && (txtPassword.getText().length() > 0)) {
                    connectServer();
                    strSend = "reg|" + txtName.getText() + "|"
                            + String.valueOf(txtPassword.getPassword());
                    out.println(strSend);
                    initLogin();
                }
            } else if (obj.equals(btnExit)) {
                System.exit(0);
            } else if (obj.equals(btnTalk)) {
                if (txtTalk.getText().length() > 0) {
                    out.println("talk|" + txtTalk.getText() + "|"
                            + txtName.getText() + "|"
                            + listOnline.getSelectedItem().toString());//
                    txtTalk.setText("");
                }
            }
        } catch (Exception e) {
            System.out.println(e);
        }
        /*尝试捕获异常的代码块，用于处理按钮点击事件可能抛出的异常。
如果 obj 对象与 btnLogin 相等，表示用户点击了登录按钮。然后检查文本框 txtServerIP、txtName 和 txtPassword 中是否有输入内容。如果都不为空，
调用 connectServer 方法连接服务器，构建登录消息字符串 strSend，并通过输出流 out 将该消息发送给服务器。然后调用 initLogin 方法初始化登录界面。
如果 obj 对象与 btnReg 相等，表示用户点击了注册按钮。然后检查文本框 txtName 和 txtPassword 中是否有输入内容。如果都不为空，
调用 connectServer 方法连接服务器，构建注册消息字符串 strSend，并通过输出流 out 将该消息发送给服务器。然后调用 initLogin 方法初始化登录界面。
如果 obj 对象与 btnExit 相等，表示用户点击了退出按钮。此时调用 System.exit(0) 方法终止程序的运行。
如果 obj 对象与 btnTalk 相等，表示用户点击了发送按钮。然后检查文本框 txtTalk 中是否有输入内容。如果不为空，构建聊天消息字符串 strSend，
包括要发送的消息文本、用户名和要发送给的在线用户。通过输出流 out 将该消息发送给服务器，并将文本框 txtTalk 清空。
如果发生异常，将异常信息打印到控制台。*/
    }
//actionPerformed 方法，处理按钮点击事件。
    class ClientThread implements Runnable {
        private Socket socket;
        private BufferedReader in;
        private PrintWriter out;
        private String strReceive, strKey;
        private Thread threadTalk;
        private StringTokenizer st;
//定义了一个名为 ClientThread 的内部类，实现了 Runnable 接口。该类用于在后台接收服务器发送的消息并更新客户端界面。
// 定义了一些私有变量，包括 socket（套接字）、in（输入流）、out（输出流）、
// strReceive（接收到的字符串消息）、strKey（解析后的消息关键字）、
// threadTalk（线程对象）和 st（用于解析消息的 StringTokenizer 对象）。
        public ClientThread(Socket s) throws IOException {
            this.socket = s;
            in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            threadTalk = new Thread(this);
            threadTalk.start();
        }
//ClientThread 类的构造函数，接受一个套接字对象作为参数。在构造函数中，
// 将传入的套接字赋值给 socket 变量，并创建一个输入流 in 对象，
// 用于从套接字中读取服务器发送的消息。然后，创建一个新的线程 threadTalk，并启动该线程。
        public void run() {
            while (true) {
                synchronized (this) {
                    try {
                        strReceive = in.readLine();
                        st = new StringTokenizer(strReceive, "|");
                        strKey = st.nextToken();
                        if (strKey.equals("talk")) {
                            String strTalk = st.nextToken();
                            strTalk = txtViewTalk.getText() + "\r\n   "
                                    + strTalk;
                            txtViewTalk.setText(strTalk);
                        } else if (strKey.equals("online")) {
                            String strOnline;
                            while (st.hasMoreTokens()) {
                                strOnline = st.nextToken();
                                listOnline.addItem(strOnline);
                            }
                        } else if (strKey.equals("remove")) {
                            String strRemove;
                            while (st.hasMoreTokens()) {
                                strRemove = st.nextToken();
                                listOnline.removeItem(strRemove);
                            }
                        } else if (strKey.equals("warning")) {
                            String strWarning = st.nextToken();
                            popWindows(strWarning, "Warning");
                        }
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    } catch (IOException e) {
                    }
                    /*run 方法，实现了 Runnable 接口的方法，用于在后台运行。该方法在一个无限循环中，不断接收服务器发送的消息并处理。
                    在每次循环中，使用 in.readLine() 从输入流中读取一行消息，并使用 StringTokenizer 对象 st 将消息按照 "|" 分隔符进行解析。
                    然后，根据解析后的关键字 strKey 进行相应的处理：
如果 strKey 为 "talk"，则将下一个标记 st.nextToken() 解析为聊天消息，并将该消息追加到文本区域 txtViewTalk 中。
如果 strKey 为 "online"，则依次解析后续标记为在线用户的名称，并将它们添加到下拉列表框 listOnline 中。
如果 strKey 为 "remove"，则依次解析后续标记为要移除的在线用户的名称，并将它们从下拉列表框 listOnline 中移除。
如果 strKey 为 "warning"，则将下一个标记 st.nextToken() 解析为警告消息，并弹出窗口显示该警告消息。
在每次循环结束后，线程会暂停 1000 毫秒（1秒）。*/
                }
            }
        }
    }
}
